package ctu.kfad.run;

import ctu.kfad.run.memory.*;
import ctu.kfad.run.memory.garbagecollector.GarbageCollector;
import ctu.kfad.run.stack.AStackObject;
import ctu.kfad.run.stack.IntegerStackObject;
import ctu.kfad.run.stack.ReferenceStackObject;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.generic.ArrayType;
import org.apache.bcel.generic.BasicType;
import org.apache.bcel.generic.ObjectType;

public class InstanceController {

    public static final String JAVA_OBJECT = "java.lang.Object";
    public static final String JAVA_STRING = "java.lang.String";
    public static final String JAVA_REFERENCE_ARRAY = "referenceArray";
    public static final String JAVA_INT_ARRAY = "intArray";
    public static final String JAVA_BOOLEAN_ARRAY = "booleanArray";

    private final ClassFileRepository repository;
    private final ClassSizeDescriptorRepository descriptorRepository;
    private final ObjectTable objectTable = new ObjectTable();
    private final MemoryManager memoryManager;

    public InstanceController(ClassFileRepository repository, MemoryManager memoryManager) {
        this.repository = repository;
        this.descriptorRepository = new ClassSizeDescriptorRepository(repository);
        this.memoryManager = memoryManager;
    }

    public void putField(ReferenceStackObject reference, ObjectType invokeClassType, String fieldName, AStackObject value) {
        final InstanceDescriptor classDescriptor = objectTable.getDescriptorForAddress(reference.getReference());
        final String instanceClassName = classDescriptor.getClassName();
        final MemoryFieldInfo info = descriptorRepository.getInfo(fieldName, invokeClassType.getClassName(), instanceClassName);
        final byte[] bytes = FieldByteConverter.covertToBytes(info.signature, value);
        memoryManager.writeToMemory(classDescriptor.getReferenceToMemory() + info.offset, bytes);
    }

    public AStackObject getField(ReferenceStackObject reference, ObjectType classType, String fieldName) {
        final InstanceDescriptor classDescriptor = objectTable.getDescriptorForAddress(reference.getReference());
        final String instanceClassName = classDescriptor.getClassName();
        final MemoryFieldInfo info = descriptorRepository.getInfo(fieldName, classType.getClassName(), instanceClassName);
        final byte[] bytes = memoryManager.readMemory(classDescriptor.getReferenceToMemory() + info.offset, info.size);
        return FieldByteConverter.covertFromBytes(info.signature, bytes);
    }

    public JavaClass loadClass(String className) {
        return repository.loadClass(className);
    }

    public JavaClass loadClass(ReferenceStackObject reference) {
        final InstanceDescriptor descriptorForAddress = objectTable.getDescriptorForAddress(reference.getReference());
        return repository.loadClass(descriptorForAddress.getClassName());
    }


    public ReferenceStackObject createInstance(ObjectType classType, IStackObjectProvider stackObjectProvider) {
        final int totalSize = descriptorRepository.getTotalSize(classType.getClassName());

        int referenceInMemory = allocateSpace(stackObjectProvider, totalSize);
        final int objectTableIndex = objectTable.saveNewObject(classType.getClassName(), referenceInMemory);
        return new ReferenceStackObject(objectTableIndex);
    }

    private int allocateSpace(IStackObjectProvider stackObjectProvider, int totalSize) {
        int referenceInMemory = memoryManager.allocateSpace(totalSize);
        if (referenceInMemory < 0) {
            new GarbageCollector(objectTable, stackObjectProvider, memoryManager, descriptorRepository).collect();
            referenceInMemory = memoryManager.allocateSpace(totalSize);
            if (referenceInMemory < 0) {
                throw new NotEnoughSpaceException();
            }
        }
        return referenceInMemory;
    }

    public ReferenceStackObject createReferenceArrayInstance(int size, IStackObjectProvider stackObjectProvider) {
        final int lengthInBytes = size * FieldValueConverter.REFERENCE_BYTE_SIZE;
        int referenceInMemory = allocateSpace(stackObjectProvider, lengthInBytes);
        final ArraySizeDescriptor arraySizeDescriptor = new ArraySizeDescriptor(size, FieldValueConverter.REFERENCE_BYTE_SIZE, lengthInBytes, JAVA_REFERENCE_ARRAY);
        int objectTableIndex = objectTable.saveNewArray(arraySizeDescriptor, referenceInMemory);
        return new ReferenceStackObject(objectTableIndex);
    }

    public ReferenceStackObject saveString(String string, IStackObjectProvider stackObjectProvider) {
        final byte[] bytes = FieldValueConverter.toBytes(string);
        int referenceInMemory = allocateSpace(stackObjectProvider, bytes.length);
        memoryManager.writeToMemory(referenceInMemory, bytes);
        final ArraySizeDescriptor arraySizeDescriptor = new ArraySizeDescriptor(string.length(), 0, bytes.length, JAVA_STRING); //I thing UTF8 encoding has variable length per character , so we cant use assumptions about size per item
        final int objectTableIndex = objectTable.saveNewArray(arraySizeDescriptor, referenceInMemory);
        return new ReferenceStackObject(objectTableIndex);
    }

    public ReferenceStackObject createArrayInstance(ArrayType type, int size, IStackObjectProvider stackObjectProvider) {
        int referenceInMemory;
        ArraySizeDescriptor descriptor;
        final int lengthInBytes = size * FieldValueConverter.INT_BYTE_SIZE;
        if (type.getBasicType().getType() == BasicType.INT.getType()) {
            descriptor = new ArraySizeDescriptor(size, FieldValueConverter.INT_BYTE_SIZE, lengthInBytes, JAVA_INT_ARRAY);
        } else if (type.getBasicType().getType() == BasicType.BOOLEAN.getType()) {
            descriptor = new ArraySizeDescriptor(size, FieldValueConverter.BYTE_BYTE_SIZE, lengthInBytes, JAVA_BOOLEAN_ARRAY);
        } else {
            throw new RuntimeException("unknown array type");
        }

        referenceInMemory = allocateSpace(stackObjectProvider, lengthInBytes);
        final int objectTableIndex = objectTable.saveNewArray(descriptor, referenceInMemory);

        return new ReferenceStackObject(objectTableIndex);
    }

    public void arrayStore(ReferenceStackObject referenceStackObject, int index, int value) {
        final InstanceDescriptor descriptor = objectTable.getDescriptorForAddress(referenceStackObject.getReference());
        final int startIndex = descriptor.getReferenceToMemory() + FieldValueConverter.INT_BYTE_SIZE * index;
        memoryManager.writeToMemory(startIndex, FieldValueConverter.toBytes(value));
    }

    public void arrayStore(ReferenceStackObject referenceStackObject, int index, byte value) {
        final InstanceDescriptor descriptor = objectTable.getDescriptorForAddress(referenceStackObject.getReference());
        final int startIndex = descriptor.getReferenceToMemory() + FieldValueConverter.BYTE_BYTE_SIZE * index;
        memoryManager.writeToMemory(startIndex, new byte[]{value});
    }

    public int arrayLoadInteger(ReferenceStackObject referenceStackObject, int index) {
        final int referenceInMemory = objectTable.findReferenceInMemory(referenceStackObject.getReference());
        final byte[] bytes = memoryManager.readMemory(referenceInMemory + FieldValueConverter.INT_BYTE_SIZE * index, FieldValueConverter.INT_BYTE_SIZE);
        return FieldValueConverter.toInt(bytes);
    }

    public byte arrayLoadByte(ReferenceStackObject referenceStackObject, int index) {
        final int referenceInMemory = objectTable.findReferenceInMemory(referenceStackObject.getReference());
        final byte[] bytes = memoryManager.readMemory(referenceInMemory + FieldValueConverter.BYTE_BYTE_SIZE * index, FieldValueConverter.INT_BYTE_SIZE);
        return bytes[0];
    }

    public String loadString(ReferenceStackObject reference) {
        final int address = reference.getReference();
        final int referenceInMemory = objectTable.findReferenceInMemory(address);
        final InstanceDescriptor descriptorForAddress = objectTable.getDescriptorForAddress(address);
        final ArraySizeDescriptor arrayDescriptor = descriptorForAddress.getArrayDescriptor();
        final byte[] bytes = memoryManager.readMemory(referenceInMemory, arrayDescriptor.getLengthInBytes());
        return FieldValueConverter.toString(bytes);
    }

    public int arrayLength(ReferenceStackObject referenceStackObject) {
        return objectTable.getDescriptorForAddress(referenceStackObject.getReference()).getArrayDescriptor().getLength();
    }


    public ReferenceStackObject objectToString(ReferenceStackObject reference, IStackObjectProvider stackObjectProvider) {
        final InstanceDescriptor descriptorForAddress = objectTable.getDescriptorForAddress(reference.getReference());
        if (JAVA_STRING.equals(descriptorForAddress.getClassName())) {
            return new ReferenceStackObject(reference.getReference());
        } else {
            return saveString(descriptorForAddress.getClassName() + "@" + reference.getReference(), stackObjectProvider);
        }
    }

    public IntegerStackObject objectHashCode(ReferenceStackObject reference) {
        return new IntegerStackObject(reference.getReference());
    }

    public IntegerStackObject objectEquals(ReferenceStackObject reference1, ReferenceStackObject reference2) {
        final String className1 = objectTable.getDescriptorForAddress(reference1.getReference()).getClassName();
        final String className2 = objectTable.getDescriptorForAddress(reference2.getReference()).getClassName();
        if (JAVA_STRING.equals(className1) && JAVA_STRING.equals(className2)) {
            return new IntegerStackObject(loadString(reference1).equals(loadString(reference2)));
        } else {
            return new IntegerStackObject(reference1.getReference() == reference2.getReference());
        }
    }

    public String loadClassName(ReferenceStackObject reference) {
        return objectTable.getDescriptorForAddress(reference.getReference()).getClassName();
    }

    public ReferenceStackObject saveStringArray(String[] stringArray, IStackObjectProvider stackObjectProvider) {
        final ReferenceStackObject referenceToArray = createReferenceArrayInstance(stringArray.length, stackObjectProvider);
        for (int i = 0; i < stringArray.length; i++) {
            final ReferenceStackObject referenceToString = saveString(stringArray[i], stackObjectProvider);
            arrayStore(referenceToArray, i, referenceToString.getReference());
        }
        return referenceToArray;
    }
}
